Post

Replies

Boosts

Views

Activity

Is there a way to opt a Catalyst app into supporting preferred text size?
As of macOS Sequoia 15.1 (and probably earlier), in System Settings under Accessibility -> Display, there's a Text Size option that looks an awful lot like Dynamic Type on iOS: I have an iOS app with robust support for Dynamic Type that I've brought to the Mac via Catalyst. Is there any way for me to opt this app into supporting this setting, maybe with some Info.plist key? Calendar's Info.plist has a CTIgnoreUserFonts value set to true, but the Info.plist for Notes has no such value.
0
0
140
2w
How can I make my multi-window Catalyst app restore window size and position after closing with stoplight button?
I have a Catalyst app that supports multiple scenes / windows. It has one "main" window type and lots of other secondary windows that can be opened. I'm using the system window restoration functionality via NSQuitAlwaysKeepsWindows to restore windows with their user activity data after the app is quit and restarted. Normally, this works great. If I open my app, change the size and position of my window, quit, and reopen it, the window size and position comes back as expected. But if I close the window using the red stoplight button and then click the app icon to bring it back, it comes back at the default position and size. This isn't how other system apps work - if I close the system Calendar app with the stoplight button, it comes back at the same size and position. How do I get this behavior with my Catalyst app? Is there some identifier property I need to set somewhere? I don't see such a property on UISceneConfiguration. If it matters, I'm using the configurationForConnectingSceneSession method to configure my windows when they open, instead of setting it up in the Info.plist.
1
0
224
3w
How can I get a tab-bar styled ornament on the trailing edge of a view controller?
I've got a UIKit app with a collapsible trailing-edge child view controller, implemented sort of like UISplitViewController but it's got a bunch of custom behavior - moving to the bottom edge in portrait orientation, etc. It exposes a couple of different app functions via a UITabBar on the bottom edge on iOS. When I run the app on visionOS, that tab bar transforms to a leading-edge ornament. This would be great, but that means it tries to overlap the trailing-edge content of its parent view controller, which isn't ideal. Is there a way to get the tab bar to lay out on the trailling edge of the child view controller? Or can I create a custom ornament that has the same auto-expand behavior as the tab bar, where it shows a vertical column of icons that expands to show titles when you're gazing at it?
1
0
188
4w
How do I make a toolbar-style window ornament from UIKit?
I've got a UIKit app and I want to add some buttons in a top-edge window ornament. I'm looking at the WWDC23 talk Meet UIKit for Spatial Computing, and it does exactly what I think I want to do: extension EditorViewController { func showEditingControlsOrnament() { let ornament = UIHostingOrnament(sceneAlignment: .bottom, contentAlignment: .center) { EditingControlsView(model: controlsViewModel) .glassBackgroundEffect() } self.ornaments = [ornament] editorView.style = .edgeToEdge } } But the thing I really want to know is what is in EditingControlsView. Is it a toolbar? How do you make a toolbar in SwiftUI without something to attach the .toolbar modifier to?
1
0
162
4w
How to migrate macOS keychain entry to new rewritten app?
I'm working on replacing an AppKit-based Mac app with one built on Catalyst, and the Catalyst app doesn't seem to be able to read the keychain item that was saved by the old app. Both apps are using the same bundle ID. The old app uses the old SecKeychain APIs - SecKeychainFindGenericPassword and friends - and the Catalyst app uses the newer SecItemCopyMatching and such. When I try using the new API in the old app to search for the entry, it works, but the exact same code in Catalyst fails. Here's how I save an item in the old app: NSString *strItemId = @"my_item_id; NSString *username = @"user"; const char *userPointer = [username UTF8String]; NSString *password = @"password"; const char *pwPointer = [password UTF8String]; SecKeychainItemRef ref = NULL; OSStatus status = SecKeychainFindGenericPassword(0, (UInt32)strlen(strItemId.UTF8String), strItemId.UTF8String, 0, NULL, NULL, NULL, &ref); if (status == errSecSuccess && ref != NULL) { //update existing item SecKeychainAttribute attr; attr.length = (UInt32)strlen(userPointer); attr.data = (void *)userPointer; attr.tag = kSecAccountItemAttr; SecKeychainAttributeList list; list.count = 1; list.attr = &attr; OSStatus writeStatus = SecKeychainItemModifyAttributesAndData(ref, &list, (UInt32)strlen(pwPointer), pwPointer); } else { status = SecKeychainAddGenericPassword(NULL, (UInt32)strlen(strItemId.UTF8String), strItemId.UTF8String, (UInt32)strlen(userPointer), userPointer, (UInt32)strlen(pwPointer), pwPointer, NULL); } And here's the query code that works in the old app but returns errSecItemNotFound in Catalyst: NSMutableDictionary *queryDict = [[[NSMutableDictionary alloc]init]autorelease]; [queryDict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; [queryDict setObject:(@"my_item_id") forKey:(__bridge id)kSecAttrService]; [queryDict setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit]; [queryDict setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes]; CFMutableDictionaryRef outDictionary = nil; OSStatus err = SecItemCopyMatching((__bridge CFDictionaryRef)queryDict, (CFTypeRef *)&outDictionary); I tried creating a new blank AppKit-based Mac app project in Xcode and gave it the old Mac app's bundle ID, and the SecItemCopyMatching query code above works there. Then I created a new iOS target with Catalyst enabled, also with the same bundle ID, and the query code running there under Catalyst returned errSecItemNotFound. So maybe the issue is something specific to Catalyst? Is there something I need to do with the Catalyst app to give it access to the old app's keychain entry, besides setting its bundle ID to match the old app?
1
0
349
Oct ’24
What is ChronoKit.InteractiveWidgetActionRunner.Errors Code 1?
This is a follow up to this post about building a Control Center widget to open the app directly to a particular feature. I have it working in a sample app, but when I do the same thing in my full app I get this error: [[com.olivetree.BR-Free::com.olivetree.BR-Free.VerseWidget:com.olivetree.BR-Free.ContinueReadingPlanControl:-]] Control action: failed with error: Error Domain=ChronoKit.InteractiveWidgetActionRunner.Errors Code=1 "(null)" Google has nothing for any of that. Can anyone shed light on what it means? This is my control and its action: @available(iOS 18.0, *) struct ContinueReadingPlanControl : ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration(kind: "com.olivetree.BR-Free.ContinueReadingPlanControl") { ControlWidgetButton(action: ContinueReadingPlanIntent()) { Image(systemName: "book") } } .displayName("Continue Reading Plan") } } @available(iOS 18.0, *) struct ContinueReadingPlanIntent : ControlConfigurationIntent { static let title: LocalizedStringResource = "Continue Reading Plan" static let description = IntentDescription(stringLiteral: "Continue the last-used reading plan") static let isDiscoverable = false static let opensAppWhenRun: Bool = true @MainActor func perform() async throws -> some IntentResult & OpensIntent { let strUrl = "olivetree://startplanday" UserDefaults.standard.setValue(strUrl, forKey: "StartupUrl") return .result(opensIntent: OpenURLIntent(URL(string: strUrl)!)) } } Note also that I'm pulling this from Console.app, streaming the logs from my device. I don't know of a way to debug a Control Center widget in Xcode, though this thread implies that it's possible.
2
0
442
Sep ’24
How does OpenIntent in a control widget actually open the app to the target feature?
I want to add a Control Center widget for my app that will open the app to a particular feature. I'm looking at the "Open your app with a control" example here, which seems like exactly what I want: Set your control’s action to an app intent that conforms to OpenIntent to open your app when someone uses a control. Using OpenIntent allows you to take someone to a specific area of your app when a control performs its action. The example doesn't show exactly how to hook up the LaunchAppIntent to a control widget, but I'm guessing it's something like this: @available(iOS 18.0, *) struct OpenFeatureControl : ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration(kind: "com.example.OpenFeature") { ControlWidgetButton(action: LaunchAppIntent()) { Image(systemName: "book") } } .displayName("Launch Feature") } } But there's one critical piece missing here: how is the target feature actually opened? My initial assumption would have been that once the app launches or resumes, there's a call to some method like continueUserActivity that has a user-info dict with some key whose value is the LaunchAppEnum. But I've put breakpoints on all those methods in my app and none of them get called (I'm using UIKit scene lifecycle). I also tried a regular AppIntent with a perform method that talks to my app directly: @available(iOS 18.0, *) struct OpenFeatureIntent : AppIntent { static let title: LocalizedStringResource = "Open My Feature" static let opensAppWhenRun: Bool = true init() {} func perform() async throws -> some IntentResult { //MAIN_APP is defined in Active Compilation Conditions in build settings #if MAIN_APP let url = URL(string: "myapp://openfeature")! UrlHandler.instance().handle(url) #endif return .result() } } But when run, this simply does nothing. Launching an app directly to a particular view or feature seems like a common use-case for control widgets, and there are apps doing it, but I can't find an example of how it's supposed to work. And the docs are really not helpful. Can anyone provide the missing piece here? What's the expected plumbing in an OpenIntent that actually launches particular UI in the app?
3
1
683
Sep ’24
How can I use the F8 play/pause key to control media playback in Catalyst?
I have a Catalyst app that plays audio via AVQueuePlayer, and I'd like to use the system play/pause key (F8 on my MacBook Pro keyboard) to play and pause it. It doesn't seem to work automatically, and if I hook up a UIKeyCommand using UIKeyInputF8, it works with Fn-F8, not F8 on its own. It does seem to work in Overcast's Mac app, but I think that's an iPad app for Mac, not Catalyst, so it's probably going through whatever system pathway that the Lock Screen controls would be using on iOS. How do I make this work on Catalyst?
1
0
411
Aug ’24
How can I include app intents only for newer OS versions?
I have an app that currently supports as low as iOS 16. I'd like to add some app intents to it that allow customization using the WidgetConfigurationIntent API that's only available on iOS 17 and later. Is there a way to build an intent (or other kind of app extension) that requires a higher OS version than my main app's deployment target, and only surface it for those OS versions?
0
0
390
Jun ’24
How can I make NSToolbar on Catalyst check canPerformAction on UISplitViewController's secondary view controller instead of primary?
I have a Catalyst app that I'm adding a sidebar to via UISplitViewController. I have a toolbar on the window with buttons that I want to be enabled or disabled based on the state of the view controller in the split view's secondary column. But it seems to want to check the primary view controller instead. In the Catalyst tutorial Adding a Toolbar, this exact approach is demonstrated. The RecipeDetailViewController has methods for toggleFavorite and editRecipe, and the toolbar items are set up to reference those selectors in the ToolbarDelegate class. In the screenshots in the tutorial, the buttons are shown as enabled based on RecipeDetailViewController.canPerformAction. But when I download and run the complete project on my computer (macOS 14.4.1), the toolbar items are disabled. And if I add the methods to the RecipeListViewController (which is in the primary column of the UISplitViewController, the toolbar items get enabled. Is there a way to make the system ask the correct split view column for canPerformAction? Or is this a bug?
1
0
476
Apr ’24
How can I put an NSToolbarItem on the leading edge of an NSToolbar in Catalyst?
I'm working to make my iOS app available via Catalyst, and I'm adding a leading-edge sidebar via UISplitViewController and putting a toggle button in the window toolbar to control it. I'd like that sidebar toggle button to go on the leading side of the toolbar, before the window title. In the Adding a Toolbar Catalyst tutorial, there are screenshots of this behavior: But when I download the finished project and run it on my machine (macOS 14.4), the toolbar buttons are clustered on the trailing edge: How do I achieve the behavior shown in the tutorial's screenshot, where the toggle sidebar button (or, ideally any custom toolbar item I choose) shows up on the leading edge? Even more ideally, is there a way I can make the sidebar toggle button show up in the header section for the sidebar, like it does in Xcode - right next to the stoplight buttons?
1
0
485
Apr ’24
How do I open my app's section of Notification settings in Catalyst?
I have a button in my iOS app that opens the Settings app to my app's Notification permissions section. I use UIApplicationOpenSettingsURLString for this (or UIApplicationOpenNotificationSettingsURLString on iOS 15.4 and later). On Catalyst, both of these simply open the settings screen that is auto-generated from the Settings bundle. How do I get it to open to the appropriate place in System Settings?
2
1
717
Dec ’23
NSTextView crash with interaction between inserted .link attribute in text storage and NSSpellChecker
I have been getting crash reports from users of my Mac app on Sonoma 14.0 and 14.1 when typing into an NSTextView subclass. The crash logs I have show involvement of the spell-checking system - NSTestCheckingController, NSSpellChecker, and NSCorrectionPanel. The crash is because of an exception being thrown. The throwing method is either [NSString getParagraphStart:end:contentsEnd:forRange:] or [NSTextStorage ensureAttributesAreFixedInRange:]. I have not yet reproduced the crash. I have tried modifying the reference finding process to simply link every word, via NSStringEnumerationByWords. The text view in question recognizes certain things in the entered text and adds hyperlinks to the text while the user is typing. It re-parses and re-adds the links on every key press (via overriding the didChangeText method), on a background thread. From user reports, I have learned that: The crash only occurs on macOS 14.0 and 14.1, not on previous versions The call stack always involves the spell checker, and sometimes involves adding recognized links to the text storage (the call to DispatchQueue.main.async in the code below) The crash stops happening if the user turns off the system spell checker in System Settings -> Keyboard -> Edit on an Input Source -> Correct Spelling Automatically switch The crash does not happen when there are no links in the text view. Here is the relevant code: extension NSMutableAttributedString { func batchUpdates(_ updates: () -> ()) { self.beginEditing() updates() self.endEditing() } } class MyTextView : NSTextView { func didChangeText() { super.didChangeText() findReferences() } var parseToken: CancelationToken? = nil let parseQueue = DispatchQueue(label: "com.myapp.ref_parser") private func findReferences() { guard let storage = self.textStorage else { return } self.parseToken?.requestCancel() let token = CancelationToken() self.parseToken = token let text = storage.string self.parseQueue.async { if token.cancelRequested { return } let refs = RefParser.findReferences(inText: text, cancelationToken: token) DispatchQueue.main.async { if !token.cancelRequested { storage.batchUpdates { var linkRanges: [NSRange] = [] storage.enumerateAttribute(.link, in: NSRange(location: 0, length: storage.length)) { linkValue, linkRange, stop in if let linkUrl = linkValue as? NSURL { linkRanges.append(linkRange) } } for rng in linkRanges { storage.removeAttribute(.link, range: rng) } for r in refs { storage.addAttribute(.link, value: r.url, range: r.range) } } self.verseParseToken = nil } } } } } I've filed this as FB13306015 if any engineers see this. Can anyone
2
0
750
Oct ’23